-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Treat methods with empty bodies in Protocols as abstract #12118
Conversation
Issues #8005 and #8926 are both based on an incorrect user belief that an empty method in a Protocol is abstract. However this does not match Python's runtime behavior. Instead of treating empty methods as abstract, MyPy should flag the method as missing a return statement, and optionally suggest adding the For example, consider this class definition:
MyPy should raise an error like this:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First of all, thanks for working on this!
But, sorry, I fail to understand this feature.
Questions I have:
- I was always sure that we use protocols to define some object's shape, not ever use them to define behavior. So, I was always thinking that all protocol methods are abstract. (Moreover, I think that they should be forced to be empty, but that's a completely different discussion). Take a look at
interface
in TypeScript. - Why a method with
-> None
is not treated asabstract
? - I think that in current state this will cause more problems that solve. For example, it would quite hard to teach which methods are abstract and which are not. I would appreciate a plan on how we can do that
That may be how most people think of protocols, but PEP 544 chose to state that they can provide non-abstract “default implementations” and classes can rely on those. Which I think has now led to this confusion about what it means to subclass a proto and how to check it. |
So, my goal was to make mypy raise an error for this case: from typing import Protocol
class Sized(Protocol):
def __len__(self) -> int: ...
class A(Sized): ...
a = A()
assert isinstance(len(a), int) # runtime error but no mypy error The problem here is the interplay of explicit protocol-implementation (i.e., subclassing of One possible solution would indeed be to force protocol methods to either provide a correct default implementation (i.e., one that returns the correct value) or to be marked as abstract: from abc import abstractmethod
from typing import Protocol
class P(Protocol):
def meth1(self) -> int: # E: Missing return statement
pass
@abstractmethod
def meth2(self) -> int: # OK
pass But I feel that goes a bit against what protocols were meant to do? I feel like default implementations in general are kind of not a good fit for protocols; they're better for abstract base classes, but I don't know. The advantage of this approach is that it works in stub files as well. The other possibility is to keep track of when empty methods are not overridden, such that when they are called, mypy knows that it can't possibly return the correct type (because empty functions always return This latter approach is kind of what I tried to do here, but I hijacked the abstract-method mechanism to achieve it. Instead of checking at the call site whether a function has an implementation (that has a chance of returning the correct type), I just mark empty functions as abstract, so that mypy can use its normal mechanism to mark methods as non-abstract when an implementation is provided. I guess this is not technically correct, because empty methods in protocols aren't technically abstract, but it does lead to the correct result, that mypy warns when an empty, non-overridden method from a protocol is called. |
2f9d921
to
e09397d
Compare
I addressed the review feedback. There is some new relevant discussion in #8926. Would it be better to remove the check of the return type? This would then be more in line with what pyright does. |
This comment has been minimized.
This comment has been minimized.
e09397d
to
74be938
Compare
Any interest in this? @ilevkivskyi |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this PR. Issues with empty bodies come up quite often. About special-casing None
: I don't think this is a good idea. In general, few people know that protocols (as well as ordinary ABCs) can have default implementations. And even among those who know and use them, few will have empty body as an intended default implementation.
Also semantic analysis is a wrong place for type-checking. The current implementation already misses couple corner cases, like type variables that can be None
, and maybe there are more.
I think it will be better to give a note that an explicit return None
must be used if it is intentional. (IIRC we give instantiation error at type-checking stage, where you can simply use is_subtype()
)
Yeah, that makes sense – I'll remove that special-case-ing.
I see. I will try to find the right place.
That sounds good. I will try to figure out how to do that. |
Closes python#8005 Closes python#8926 Methods in Protocols are considered abstract if they have an empty function body, have a return type that is not compatible with `None`, and are not in a stub file.
74be938
to
614c9cc
Compare
This comment has been minimized.
This comment has been minimized.
Not sure why this is needed.
@ilevkivskyi I implemented your suggestions (hopefully I understood them correctly), but there is a problem with The other problem is that the I'm also confused why I had to add I didn't use an EDIT: the error in https://github.com/yelp/paasta seems like a genuine error in their code; I already sent a PR. |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! This looks better, I have some more detailed comments now.
Maybe stubgen automatically adds some imports if it sees an abstract class. In any case, I wouldn't touch |
- rename some variables and functions - treat overloads without implementation as explicitly abstract - don't show the note for methods without type annotations - add more comments to explain things - don't rely on specific constants being falsey - fix stubgen
I think I addressed everything. The code looks cleaner now, I think. The only potential problem I see now is the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't worry too much about non-strict optional. Few people use it now, and it is just a usability issue, not correctness.
I have one more suggestion, after you fix it I will merge when tests pass.
mypy/messages.py
Outdated
@@ -1330,6 +1330,15 @@ def cannot_instantiate_abstract_class( | |||
context, | |||
code=codes.ABSTRACT, | |||
) | |||
for a, can_return_none_and_implicit in abstract_attributes.items(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of giving a separate node for each method. I would instead give a single note using format_string_list()
like we do few lines above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. If you have suggestions on re-wording the message in any way, let me know.
This comment has been minimized.
This comment has been minimized.
Diff from mypy_primer, showing the effect of this PR on open source code: paasta (https://github.com/yelp/paasta)
+ paasta_tools/metrics/metrics_lib.py:174: error: Cannot instantiate abstract class "Counter" with abstract attribute "set"
|
Description
closes #8005
closes #8926
Based on the quick fix idea from this comment by @ilevkivskyi .
For the problem description see #8005. The underlying problem is arguably #2350, but that seems not so easy to fix (there was an attempt in #8111 but it was abandoned). For this PR, I borrowed a lot from #8111 but kept the scope much less ambitious (for example, #8111 also warns on
super()
calls to empty functions, which this PR doesn't). However, as pointed out here, empty bodies are actually a valid implementation if the function may returnNone
, so I added that as an additional check. But if that is deemed too confusing, I'm happy to remove that check.So, in summary, what this PR does: Methods in Protocols are considered abstract if they
None
andTest Plan
Added tests for all the new behavior.